package service

import (
	"archive/zip"
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"sync"
)

func NewCompileAgent(name, compileEnvZipPath, runningHomeBase string, cmd []string) *CompileAgent {
	return &CompileAgent{
		name:            name,
		compileEnvZip:   compileEnvZipPath,
		runningHomeBase: runningHomeBase,
		mutex:           sync.Mutex{},
		cmd:             cmd,
	}
}

type CompileAgent struct {
	name            string
	compileEnvZip   string
	runningHomeBase string
	mutex           sync.Mutex
	cmd             []string
}

func (a *CompileAgent) GetRunningHome() string {
	return filepath.Join(a.runningHomeBase, a.name)
}

func (a *CompileAgent) Init() error {
	a.mutex.Lock()
	defer a.mutex.Unlock()

	runningHome := a.GetRunningHome()

	fmt.Printf("running home: %v\n", runningHome)

	// 先清空文件夹
	if err := os.RemoveAll(runningHome); err != nil {
		return fmt.Errorf("clean up compile agent runningHome [%v] err, %v", runningHome, err)
	}

	if err := os.MkdirAll(runningHome, 0666); err != nil {
		return fmt.Errorf("create compileAgent running home [%v] err, %v", runningHome, err)
	}

	// 解压zip到running home
	envZip, err := os.OpenFile(a.compileEnvZip, os.O_RDONLY, 0666)
	if err != nil {
		return fmt.Errorf("open compileEnvZip [%v] err, %v", a.compileEnvZip, err)
	}
	defer envZip.Close()

	envZipStat, err := envZip.Stat()
	if err != nil {
		return fmt.Errorf("get env zip stat err, %v", err)
	}

	zipReader, err := zip.NewReader(envZip, envZipStat.Size())
	if err != nil {
		return fmt.Errorf("new zip reader of compileZipEnv [%v] err, %v", a.compileEnvZip, err)
	}

	// 创建文件夹
	for _, f := range zipReader.File {
		if f.FileInfo().IsDir() {
			dirPath := filepath.Join(runningHome, f.Name)
			if err := os.MkdirAll(dirPath, 0666); err != nil {
				return fmt.Errorf("create compileAgent subdir [%v] err, %v", dirPath, err)
			}
		}
	}

	// 写入文件
	for _, f := range zipReader.File {
		if !f.FileInfo().IsDir() {
			filename := filepath.Join(runningHome, f.Name)
			fileReader, err := f.Open()
			if err != nil {
				return fmt.Errorf("open file [%v] in zip err, %v", filename, err)
			}
			diskFile, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
			if err != nil {
				fileReader.Close()
				return fmt.Errorf("open corresponding disk file [%v] of zip file err, %v", filename, err)
			}
			_, err = io.Copy(diskFile, fileReader)
			if err != nil {
				fileReader.Close()
				diskFile.Close()
				return fmt.Errorf("write content to disk file [%v] err, %v", filename, err)
			}
			fileReader.Close()
			diskFile.Close()
		}
	}
	return nil
}

const (
	UserCodeDir  = "entry/src/main/js/default/pages/index"
	BuildBaseDir = "entry/build"
)

/*
输入用户代码压缩包，格式：
index.css
index.hml
index.js

输出jsBundle，格式：
entry/build/xxxx
entry/build.gradle
...
*/

func (a *CompileAgent) Compile(code []byte) ([]byte, string, error) {
	a.mutex.Lock()
	defer a.mutex.Unlock()

	runningHome := a.GetRunningHome()
	userCodeDir := filepath.Join(runningHome, UserCodeDir)

	// 先清空文件夹
	if err := os.RemoveAll(userCodeDir); err != nil {
		return nil, "", fmt.Errorf("clean up user code dir [%v] err, %v", userCodeDir, err)
	}
	if err := os.MkdirAll(userCodeDir, 0666); err != nil {
		return nil, "", fmt.Errorf("create user code dir [%v] err, %v", userCodeDir, err)
	}

	size := int64(len(code))
	codeReader := bytes.NewReader(code)
	zipReader, err := zip.NewReader(codeReader, size)
	if err != nil {
		return nil, "", fmt.Errorf("new zip reader err, %v", err)
	}

	for _, f := range zipReader.File {
		if !f.FileInfo().IsDir() {
			filename := filepath.Join(userCodeDir, f.Name)
			fileReader, err := f.Open()
			if err != nil {
				return nil, "", fmt.Errorf("open user code file [%v] in zip err, %v", filename, err)
			}
			diskFile, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
			if err != nil {
				fileReader.Close()
				return nil, "", fmt.Errorf("open corresponding user code disk file [%v] of zip file err, %v", filename, err)
			}
			_, err = io.Copy(diskFile, fileReader)
			if err != nil {
				fileReader.Close()
				diskFile.Close()
				return nil, "", fmt.Errorf("write user code content to disk file [%v] err, %v", filename, err)
			}
			fileReader.Close()
			diskFile.Close()
		}
	}

	// 执行编译命令
	cmd := append([]string{"/C", "cd", "/d", runningHome, "&&"}, a.cmd...)
	c := exec.Command("cmd", cmd...)
	// defer c.Process.Kill()
	outputbuf, err := c.CombinedOutput()
	output := string(outputbuf)
	fmt.Printf("%v compile output: %v, err: %v\n", a.name, output, err)
	if err != nil {
		return nil, output, fmt.Errorf("execute build command err, %v", err)
	}

	// 收集jsBundle
	jsBundle := bytes.NewBuffer(nil)
	zipWriter := zip.NewWriter(jsBundle)

	buildDirPath := filepath.Join(runningHome, BuildBaseDir)
	buildDir, err := os.Open(buildDirPath)
	if err != nil {
		return nil, output, fmt.Errorf("open build dir err, %v", err)
	}

	_, err = zipWriter.Create("entry/")
	if err != nil {
		return nil, output, fmt.Errorf("create entry dir in zip file err, %v", err)
	}

	err = compress(buildDir, "entry", zipWriter)
	if err != nil {
		return nil, output, fmt.Errorf("compress build dir err, %v", err)
	}
	if err = zipWriter.Close(); err != nil {
		return nil, output, fmt.Errorf("close zip writer err, %v", err)
	}

	jsbuf, err := ioutil.ReadAll(jsBundle)
	if err != nil {
		return nil, output, fmt.Errorf("read jsbundle err, %v", err)
	}

	return jsbuf, output, nil
}

func compress(file *os.File, prefix string, zw *zip.Writer) error {
	info, err := file.Stat()
	if err != nil {
		return err
	}
	if info.IsDir() {
		prefix = prefix + "/" + info.Name()
		fileInfos, err := file.Readdir(-1)
		if err != nil {
			return err
		}
		for _, fi := range fileInfos {
			f, err := os.Open(file.Name() + "/" + fi.Name())
			if err != nil {
				return err
			}
			err = compress(f, prefix, zw)
			if err != nil {
				return err
			}
		}
	} else {
		header, err := zip.FileInfoHeader(info)
		if err != nil {
			return err
		}
		filename := prefix + "/" + header.Name
		writer, err := zw.Create(filename)
		if err != nil {
			return err
		}
		_, err = io.Copy(writer, file)
		file.Close()
		if err != nil {
			return err
		}
	}
	return nil
}
